/**
 * ...
 * @author waneck
 */

package cmtc.viz.source.query;
#if macro
import haxe.macro.Context;
import haxe.macro.Expr;
#end

class Query<T>
{
	private var path:Array<String>;
	private var constraint:Constraint;
	private var _and:Array<Query<Dynamic>>;
	private var _or:Array<Query<Dynamic>>;
	private var _orderBy:{field:String, asc:Bool};
	
	public function new(path:Q<T>, constraint:Constraint) 
	{
		this.path = path.flatten();
		this.constraint = constraint;
		_and = [];
		_or = [];
	}
	
	public function toString()
	{
		var sb = new StringBuf();
		sb.add("SELECT * FROM ");
		sb.add(path.join("#"));
		sb.add(" WHERE ");
		sb.add(constraint);
		if (_and.length > 0)
		{
			sb.add (" AND (");
			sb.add(_and.join(" AND "));
			sb.add(")");
		}
		
		if (_or.length > 0)
		{
			sb.add (" OR (");
			sb.add(_or.join(" OR "));
			sb.add(")");
		}
		
		return sb.toString();
	}
	
	public function and(q:Query<Dynamic>)
	{
		_and.push(q);
		return this;
	}
	
	public function or(q:Query<Dynamic>)
	{
		_or.push(q);
		return this;
	}
	
	public function orderBy(field:String, asc:Bool)
	{
		this._orderBy = {field:field, asc:asc};
	}
	
	@:macro public static function make(e:Expr):Expr
	{
		var validate;
		validate = function(e:Expr)
		{
			return switch(e.expr)
			{
				case EParenthesis(e):
					validate(e);
				case EDisplay(_):
					e;
				case EField(_, _), EType(_, _):
					var all = mk(EType(mk(EConst(CType("Constraint")), e.pos), "All"), e.pos);
					mk(ECall(mk(EField( stripInstanceAcc(e) , "query"), e.pos), [ all ]), e.pos);
				default:
					mapBoolExp(e, stripInstanceAcc);
			}
		}
		return validate(e);
	}
	
	#if macro
	static function mk (e:ExprDef, pos:Position):Expr
	{
		return { expr:e, pos:pos };
	}
	
	static function stripInstanceAcc (e:Expr)
	{
		switch (e.expr)
		{
			case EField(e, f):
				if (f  == "instance")
					return e;
				else
					return mk(EField(stripInstanceAcc(e), f), e.pos);
			case EParenthesis(e):
				return mk(EParenthesis(stripInstanceAcc(e)), e.pos);
			case EType(e, f):
				if (f  == "instance")
					return e;
				else
					return mk(EType(stripInstanceAcc(e), f), e.pos);
			default:
				trace(e);
				return e;
		}
	}
	
	static function queryValFromExp (e1:Expr):Expr
	{
		return switch (e1.expr)
		{
			case EParenthesis (e):
				queryValFromExp(e);
			case EConst(c):
				switch(c)
				{
					case CInt(v), CFloat(v):
						mk(ECall(mk(EType(mk(EConst(CType("QueryValue")), e1.pos), "QFloat"), e1.pos), [mk (EConst(CFloat(v)), e1.pos) ]), e1.pos);
					case CString(s):
						mk(ECall(mk(EType(mk(EConst(CType("QueryValue")), e1.pos), "QString"), e1.pos), [e1]), e1.pos);
					default:
						Context.error("Invalid constant ", e1.pos);
						null;
				}
			default:
				mk(ECall(mk(EType(mk(EConst(CType("QueryValue")), e1.pos), "QField"), e1.pos), [mapBoolExp(e1, stripInstanceAcc)]), e1.pos);
		}
		return null;
	}
	
	static function mapBoolExp (e:Expr, fn:Expr->Expr)
	{
		switch(e.expr)
		{
			case EParenthesis(e):
				return mapBoolExp(e, fn);
			case EBinop(op, e1, e2):
				switch (op)
				{
					case OpBoolAnd, OpBoolOr:
						var o = switch(op) { case OpBoolAnd: "and"; default: "or"; };
						return (mk(ECall( mk(EField(mapBoolExp(e1, fn), o), e.pos), [mapBoolExp(e2, fn)]), e.pos));
					case OpEq, OpNotEq, OpGt, OpGte, OpLt, OpLte:
						var optype = function(qv)
						{
							return switch(op)
							{
								case OpEq:
									mk(ECall(mk(EType(mk(EConst(CType("Constraint")), e.pos), "Equal"), e.pos), [qv]), e.pos);
								case OpNotEq:
									mk(ECall(mk(EType(mk(EConst(CType("Constraint")), e.pos), "NotEqual"), e.pos), [qv]), e.pos);
								case OpGt:
									mk(ECall(mk(EType(mk(EConst(CType("Constraint")), e.pos), "Greater"), e.pos), [qv, mk(EConst(CIdent("false")), e.pos)]), e.pos);
								case OpGte:
									mk(ECall(mk(EType(mk(EConst(CType("Constraint")), e.pos), "Greater"), e.pos), [qv, mk(EConst(CIdent("true")), e.pos)]), e.pos);
								case OpLt:
									mk(ECall(mk(EType(mk(EConst(CType("Constraint")), e.pos), "Less"), e.pos), [qv, mk(EConst(CIdent("false")), e.pos)]), e.pos);
								case OpLte:
									mk(ECall(mk(EType(mk(EConst(CType("Constraint")), e.pos), "Less"), e.pos), [qv, mk(EConst(CIdent("true")), e.pos)]), e.pos);
								default:
									null;
							}
						}
						var fquery = mk(ECall(mk(EField(fn(e1), "query"), e.pos), [optype(queryValFromExp(e2))]), e.pos);
						
						
						
						return fquery;
					
					default:
						Context.error("Unexpected operator "  + op, e.pos);
						return null;
				}
			default:
				trace(e);
				return null;
		}
	}
	
	static function getStackArrayFieldAccess(e:Expr):Expr
	{
		trace(e);
		return e;
	}
	
	#end
}

class Q<Z> implements Dynamic<Q<Z>>
{
	public static function uery<T>(cl:Class<T>):Q<T>
	{
		return new Q(cl, []);
	}
	
	private var cl:Class<Z>;
	private var path:Array<String>;
	
	private function new(cl:Class<Z>, path:Array<String>)
	{
		this.cl = cl;
		this.path = path;
	}
	
	public function query(c:Constraint):Query<Z>
	{
		return new Query(this, c);
	}
	
	public function flatten():Array<String>
	{
		return [Type.getClassName(cl)].concat(path);
	}
	
	public function resolve(str:String)
	{
		return new Q<Z>(cl, path.concat([str]));
	}
	
	public var instance(get_instance, null):Z;
	
	private function get_instance():Z
	{
		throw "You forgot to use Query.make on this expression ! ";
		return null;
	}
}

enum Constraint
{
	Equal(to:QueryValue);
	NotEqual(to:QueryValue);
	Greater(than:QueryValue, eq:Bool);
	Less(than:QueryValue, eq:Bool);
	All;
}

enum QueryValue
{
	QFloat(fl:Float);
	QString(str:String);
	QField(q:Q<Dynamic>);
}